#!/usr/bin/python
# GUI to interactively control Sensor and ImageDist components
# Sensor:
# - start/stop/record receiving remote sensor values
# - add/remove filters, construct filter chain
#   a filter is any function in any module (*.py file) 
#   below the filters directory 
# - start/stop relaying filtered sensor values
# ImgDist:
# - start/stop receiving images 

import sys, os, time, imp, types
try:
    from Tkinter import *
    from ImageTk import BitmapImage, PhotoImage
except:
    from tkinter import *
import Sensor 
import ImgDist

MAX_FILTERS = 5
COLOR_RED = "#FF0000"
COLOR_GREEN = "#00FF00"
COLOR_BLUE = "#0000FF"
COLOR_LIGHTBLUE = "#BBCCFF"
COLOR_WHITE = "#FFFFFF"
COLOR_GRAY = "#DDDDDD"
COLOR_DARKGRAY = "#AAAAAA"
LOG_HISTORY = 50 # must be greater than greatest possible window size


IMG_DOT = "#define empty_width 10\n#define empty_height 10\nstatic char empty_bits[] = {\n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \n  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, };\n"
def _get_dot(color=COLOR_GRAY):
    "create an image of a small square rectangle"
    return BitmapImage(background=color, data=IMG_DOT)

class OnOffButton(Checkbutton):
    "An on/off button with indicator and state variable"

    def __init__(self, master, text, command, indicator=False,
                 color_on=COLOR_GREEN, color_off=COLOR_GRAY):
        self.on_dot = _get_dot(color=color_on)
        self.off_dot = _get_dot(color=color_off)
        Checkbutton.__init__(self, master, 
                             compound=LEFT,
                             anchor=N+W,
                             padx=4,
                             text=" " + text, 
                             indicator=indicator,
                             )
        self.state = IntVar()
        if not self["indicator"]:
            self["image"] = self.off_dot
        self.command = command
        self["command"] = self.switch

    def turn_on(self):
        "turn button on"
        self.state.set(True)
        if not self["indicator"]:
            self["image"] = self.on_dot
        self.update_command()

    def turn_off(self):
        "turn button off"
        self.state.set(False)
        if not self["indicator"]:
            self["image"] = self.off_dot
        self.update_command()

    def get(self):
        "get the current state, on/off"
        return self.state.get()

    def switch(self):
        "on->off or off->on"
        if self.state.get():
            self.turn_off()
        else:
            self.turn_on()

    def update_command(self):
        "called on change of the button state"
        self.command(self.state.get())
        

def id_filter(block): 
    "filter that doesn't do anything"
    return block


class Filters(object):
    "Manages all available filters"

    def __init__(self, directory=None):
        self.filters = dict()
        if directory == None:
            directory = "filters"
        self.order = []
        self.active = []
        try:
            _, _, names = os.walk(directory).next()
        except StopIteration:
            print("there is no subdirectory filters, no filters available")
            self.filters = {'id' : {'name' : 'id', 
                                    'func': id_filter,
                                    'mod_reload' : lambda: None}}
            return
        if "order" in names:
            lines = file(os.path.join(directory, "order")).readlines()
            self.order = [line.strip() for line in lines if line.strip()]
        if "active" in names:
            lines = file(os.path.join(directory, "active")).readlines()
            self.active = [line.strip() for line in lines if line.strip()]
        self.filter_path = directory
        sys.path.append(self.filter_path)
        names = [name for name in names if name.endswith(".py")]
        for name in names:
            name = name[:-3] # get rid of .py
            try:
                res = imp.find_module(name)
                mod = imp.load_module(name, *res)
                attrs = [getattr(mod, f) for f in dir(mod) 
                         if not f.startswith("_")]
                funcs = [f for f in attrs if type(f) == types.FunctionType]
                for func in funcs:
                    # filter_func to call functions of reloaded modules
                    def filter_func(block, 
                                    _mod=mod, _func_name=func.__name__):
                        return getattr(_mod, _func_name)(block)
                    self.filters[func.__name__] = {
                        'name' : func.__name__,
                        'func' : filter_func,
                        'mod_reload' : lambda mod=mod: imp.reload(mod)
                        }
            except ImportError as data:
                print("damn, module not found", data)
        self.active = [a for a in self.active if a in self.filters.keys()]
        del sys.path[-1] # restore original path

    def get_names(self):
        "get the sorted names of available filters"
        keys = self.filters.keys()
        names = []
        for key in self.order:
            if key in keys:
                keys.remove(key)
                names.append(key)
        names.extend(sorted(keys))
        return names

    def get_initial_active(self):
        "get the filters that should be initially active"
        return self.active 

    def get_function(self, name):
        "get the function to apply to the block"
        if name not in self.filters:
            return None
        return self.filters[name]['func']

    def reload_modules(self):
        "reload the modules, automatically new function is called (filter_func)"
        sys.path.append(self.filter_path)
        mods = []
        for fil in self.filters:
            mod_reload = self.filters[fil]['mod_reload']
            if mod_reload not in mods:
                mods.append(mod_reload)
                mod_reload()
        del sys.path[-1] # restore original path


class FilterSelect(Frame):
    "one selection of a filter including on/off checkmark"

    def __init__(self, master, sensor, filters, fid=0, logger=None):
        Frame.__init__(self, master)
        self.sensor = sensor
        self.filters = filters
        self.fid = fid
        self.logger = logger
        self.create_widgets()
        names = self.filters.get_names()
        if fid < len(names):
            self.choice.set(names[fid])
        else:
            self.choice.set(names[0])

    def create_widgets(self):
        "create on dropdown and clickable"
        self.check = OnOffButton(self, "", self.update_onoff, True)
        self.check.pack(side=LEFT)
        names = self.filters.get_names()
        self.choice = StringVar()
        self.choice.trace_variable("w", self.trace_variable)
        self.choose = OptionMenu(self, self.choice, *names)
        self.choose.pack(side=LEFT)

    def trace_variable(self, *pos):
        "changes in selection of drop down filter"
        if self.check.get():
            self.check.update_command()

    def update_onoff(self, state):
        "unregister/register filter"
        if state:
            filter_function = self.filters.get_function(self.choice.get())
            self.sensor.register_filter(filter_function, self.fid)
            if self.logger:
                self.logger.add("Registered filter", 
                                self.fid, 
                                self.choice.get())
        else:
            self.sensor.register_filter(id_filter, self.fid)
            if self.logger:
                self.logger.add("Unregistered filter", 
                                self.fid, 
                                self.choice.get())

    def current(self):
        "currently selected filter"
        return self.choice.get()

    def invoke(self):
        "invoke the checkbutton"
        self.check.invoke()


class Logger(Text):
    "A text logger to visualize log messages"

    def __init__(self, master, height=10, history=LOG_HISTORY):
        Text.__init__(self, master, 
                      undo=False,
                      height=height,
                      width=100,
                      state=DISABLED,
                      takefocus=False,
                      wrap=NONE
                      )
        self.lines = 0
        self.history = history
        self.del_lines = 10 # that much above history? delete that much
        self.start_time = time.time()

    def add(self, line, *pos):
        "add one line plus all stringified arguments in pos"
        self["state"] = NORMAL
        if self.lines == self.history+self.del_lines:
            self.lines -= self.del_lines
            self.delete("1.0", str(1+self.del_lines)+".0")
        act_time = time.time() - self.start_time
        to_add = "%9.3f: " % act_time
        to_add += line.strip()+ " " + " ".join(map(str, pos)) + "\n"
        self.insert(END, to_add)
        self.see(END)
        self.lines += 1
        self["state"] = DISABLED


class VisualLogger(Canvas):
    "Visualize as bars a couple of labeled values"
    FIRST_LABEL = 15
    NEXT_LABEL = 50
    LABEL_WIDTH = 5
    BAR_WIDTH = 10    
    # USE_COLORS = [COLOR_DARKGRAY]
    USE_COLORS = [COLOR_RED, COLOR_GREEN, COLOR_BLUE]
    HISTORY = 160
    
    def __init__(self, master, follow_vars=("x", "y", "z")):
        Canvas.__init__(self, master, 
                        width=160, height=60, 
                        bg="white")
        self.cwidth = int(self.cget("width"))
        self.cheight = int(self.cget("height"))
        self.middle = self.cheight/2
        self.follow_vars = follow_vars
        self.follow = dict()
        x_pos = self.FIRST_LABEL
        LB_WIDTH = self.LABEL_WIDTH + self.BAR_WIDTH
        coli = 0
        for var in self.follow_vars:
            color = self.USE_COLORS[coli]
            coli = coli+1 if len(self.USE_COLORS) > coli+1 else 0
            self.follow[var] = {'name': var, 'x': x_pos , 
                                'max' : 0.1, 'samplemax': 0.1,
                                'color': color,
                                'history': [] }
            rect = self.create_rectangle(x_pos + self.LABEL_WIDTH,  
                                         self.middle-2, 
                                         x_pos + LB_WIDTH, 
                                         self.middle+2,
                                         fill=color, 
                                         outline=color,
                                         width=0)
            self.follow[var]["rect"] = rect
            self.follow[var]["upd_max_counter"] = 0
            label = self.create_text(x_pos, self.middle,
                                     anchor=E, text=var, fill=color)
            self.follow[var]["label"] = label
            self.follow[var]["labelx"] = x_pos
            mti = self.create_text(x_pos, 0, anchor=N+W, 
                                   text=self.follow[var]['max'])
            self.follow[var]['max_text_id'] = mti
            x_pos += self.NEXT_LABEL
        self.bars = True
        self.bind("<Configure>", self.resize_height)
        self.bind("<Button-1>", self.switch_bars)

    def resize_height(self, event):
        "in case the visual is resized adjust the display"
        self.cheight = event.height
        self.middle = self.cheight/2
        for var in self.follow:
            label = self.follow[var]['label']
            x_pos = self.follow[var]['labelx']
            self.coords(label, x_pos, self.middle)
            rect = self.follow[var]['rect']
            self.coords(rect, 
                        x_pos+self.LABEL_WIDTH, 
                        self.middle-2, 
                        x_pos+self.LABEL_WIDTH + self.BAR_WIDTH, 
                        self.middle+2)

    def switch_bars(self, event):
        "switch between bars and lines visualization"
        if self.bars:
            self.bars = False
            for key in self.follow:
                dic = self.follow[key]
                self.coords(dic['rect'], -2,0,-1,0) # off screen
        else:
            self.bars = True
            for key in self.follow:
                dic = self.follow[key]
                if 'line' in dic:
                    self.delete(dic['line'])
        # ToDo - restore last view? might be sync trouble..
        
    def update_dic(self, dic):
        "update the values found in dic"
        for key in self.follow:
            if key in dic:
                self.update_follow(self.follow[key], float(dic[key]))

    def update_follow(self, dic, value):
        "Update one value represented internally by dic"
        value = float(value)
        # adjust scale that we see something
        if dic['max'] < abs(value):
            text = int(abs(value)) if abs(value) >= 10 else "%3.1f"%abs(value)
            self.itemconfigure(dic['max_text_id'], text=text)
            dic['max'] = abs(value)
            dic['samplemax'] = 0
            dic['upd_max_counter'] = 0
        else:
            if dic["upd_max_counter"] == 40: # every ith time
                dic["upd_max_counter"] = 0
                # newmax = 0.95*dic['max']
                newmax = dic['samplemax']
                if newmax > abs(value) and newmax >= 0.1:
                    anm = abs(newmax)
                    text = int(anm) if anm >= 10 else "%3.1f" % anm
                    self.itemconfigure(dic['max_text_id'], text=text)
                    dic['max'] = newmax # try to come down again...
                    dic['samplemax'] = 0
            else:
                if dic['samplemax'] < abs(value):
                    dic['samplemax'] = abs(value)
                dic["upd_max_counter"] += 1
        dic['history'].append(value)
        if len(dic['history']) > self.HISTORY:
            del dic['history'][0]
        # visualize value
        bars = self.bars
        if bars:
            scale = -value/dic['max']
            x0, y0 = dic["x"], self.middle
            x1 = dic["x"] + self.BAR_WIDTH,
            y1 = self.middle+int(scale*self.middle)
            rect = dic['rect']
            self.coords(rect, x0, y0, x1, y1)
            if 'line' in dic: # leftover
                self.delete(dic['line'])
                del dic['line']
        else: # line diagram
            his = dic['history']
            dmax = dic['max']
            middle = self.middle
            line = []
            for i in range(len(his)):
                line.append(i) # x
                value = middle + int((-his[i]/dmax)*middle)
                line.append(value) # y
            if 'line' in dic:
                self.delete(dic['line'])
                del dic['line']
            if len(line) >= 4: 
                dic['line'] = self.create_line(*line, fill=dic['color'])

class Header(LabelFrame):
    "Simple Header"

    def __init__(self, master, text):
        LabelFrame.__init__(self, master, text="Control", bg=COLOR_WHITE)
        self.label = Label(self, text="  " + text + "  ", 
                           anchor=CENTER, bg=COLOR_WHITE)
        self.label.grid(sticky=S)


class Input(Frame):
    "Interactively change a couple of key/value pairs"

    def __init__(self, master, vardict, bgdict=None):
        Frame.__init__(self, master)
        self.widgets = dict()
        i = 0
        self.bgdict = bgdict
        self.vardict = vardict
        for vkey in sorted(vardict.keys()):
            label = Label(self, text=vkey, width=12, anchor=E)
            label.grid(row=i, column=0)
            var = StringVar()
            var.set(vardict[vkey])
            entry = Entry(self, textvariable=var, width=10)
            entry.grid(row=i, column=1)
            self.widgets[vkey] = (label, var, entry)
            i += 1
        self.update_button = Button(self, pady=0, text="Update", 
                                    command=self.update_entries)
        self.update_button.grid(row=0, column=3, pady=0)

    def enable(self):
        "make entries editable"
        self.update_button["state"] = NORMAL
        for vkey in self.widgets:
            _, var, entry = self.widgets[vkey]
            if self.bgdict and vkey in self.bgdict:
                entry["background"] = self.bgdict[vkey](var.get(), NORMAL)
            entry["state"] = NORMAL

    def disable(self):
        "make entries read only"
        self.update_button["state"] = DISABLED
        for vkey in self.widgets:
            _, var, entry = self.widgets[vkey]
            if self.bgdict and vkey in self.bgdict:
                bgcolor = self.bgdict[vkey](var.get(), DISABLED)
                entry["disabledbackground"] = bgcolor
            entry["state"] = DISABLED


    def set_values(self, vardict):
        "copy the values found in vardict to the internal settings"
        for varkey in vardict:
            if varkey not in self.widgets:
                raise Exception("Input:set_values, var "+varkey+" missing")
            _, var, _ = self.widgets[varkey]
            var.set(vardict[varkey])

    def get_values(self, vardict):
        "copy the internal values to the values in vardict"
        for varkey in vardict:
            if varkey not in self.widgets:
                raise Exception("Input:get_values, var "+varkey+" missing")
            _, var, _ = self.widgets[varkey]
            vardict[varkey] = var.get()

    def update_entries(self):
        "copy the display variables to the internal variables"
        self.get_values(self.vardict)


class KeyVisualizer(Frame):
    "Visualize keys pressed"

    def __init__(self, master, keys="lrudf0123456789*#"):
        Frame.__init__(self, master)
        self.keys = keys
        self.create_widgets()

    def create_widgets(self):
        "create all keys as labels"
        self.keysvis = {}
        self.key_color = {}
        for key in self.keys:
            self.keysvis[key] = Label(self, text=key, bd=0)
            self.keysvis[key].pack(side=LEFT)
            self.key_color[key] = COLOR_GRAY

    def update_dic(self, dic):
        "visualize all pressed keys in a block"
        if "k" in dic:
            self.update_pressed(dic["k"])

    def update_pressed(self, keys):
        "set bg colors for the pressed/unpressed keys"
        for key in self.keys:
            if key in keys:
                if self.key_color[key] != COLOR_RED:
                    self.keysvis[key]["bg"] = COLOR_RED
                    self.key_color[key] = COLOR_RED
            else:
                if self.key_color[key] != COLOR_GRAY:
                    self.keysvis[key]["bg"] = COLOR_GRAY
                    self.key_color[key] = COLOR_GRAY


class UISensor(Frame):
    "The main display of sensor interaction"

    def __init__(self, master=None, filters=None, quitf=None):
        Frame.__init__(self, master, bd=2)
        self.filters = filters
        self.sensor = Sensor.Sensor()
        self.vardict = {'IN_PORT' : Sensor.IN_PORT,
                        'OUT_HOST': Sensor.OUT_HOST,
                        'OUT_PORT': Sensor.OUT_PORT}
        def bluesmaller32(v, state):
            if state == NORMAL:
                return COLOR_WHITE
            try:
                v = int(v)
                if v < 32:
                    return COLOR_LIGHTBLUE
            except:
                pass
            return COLOR_GRAY
        self.bgdict = {'IN_PORT': bluesmaller32}
        self.create_widgets(quitf)
        for _ in range(MAX_FILTERS): # empty slots
            self.sensor.register_filter(id_filter) # identity
        active = self.filters.get_initial_active()
        for fsel in self.filter_selects: # activate preset
            if fsel.current() in active:
                fsel.invoke()
                active.remove(fsel.current())
        self.accept_button.invoke() # accept on
        self.send_button.invoke() # send on
        self.send_vis_button.invoke() # visualize sent data on

    def create_widgets(self, quitf):
        "creates the entire panel"
        # Logger
        self.logger = Logger(self, height=12)
        self.logger.grid(row=3, column=0, sticky=N+S+E+W)
        # Button Frame
        self.button_frame = Frame(self)
        self.button_frame.grid(row=0, column=0, columnspan=4, sticky=E+W+N)
        self.header = Header(self.button_frame, text="Sensor")
        self.header.grid(row=0, rowspan=2, column=0, sticky=N+S)
        self.accept_button = OnOffButton(self.button_frame, 
                                         "Accept Sensor Data", 
                                         self.accept_data)
        self.accept_button.grid(row=0, column=1, sticky=W+E)
        self.accept_log_button = OnOffButton(self.button_frame, 
                                             "Log Accepted Data", 
                                             self.accept_log_data)
        self.accept_log_button.grid(row=1, column=1, sticky=W+E)
        self.accept_vis_button = OnOffButton(self.button_frame, 
                                             "Visualize Accepted Data", 
                                             self.accept_vis_data)
        self.accept_vis_button.grid(row=3, column=1, sticky=W+E)
        self.send_button = OnOffButton(self.button_frame, 
                                       "Send Filtered Sensor Data", 
                                       self.send_data)
        self.send_button.grid(row=0, column=2, sticky=W+E)
        self.send_log_button = OnOffButton(self.button_frame, 
                                           "Log Sent Data", 
                                           self.send_log_data)
        self.send_log_button.grid(row=1, column=2, sticky=W+E)
        self.send_vis_button = OnOffButton(self.button_frame, 
                                           "Visualize Sent Data", 
                                           self.send_vis_data)
        self.send_vis_button.grid(row=3, column=2, sticky=W+E)
        self.record_button = OnOffButton(self.button_frame,
                                         "Record",
                                         self.record,
                                         color_on=COLOR_RED)
        self.record_button.grid(row=0, column=3, sticky=W+E)
        # some settings
        self.settings = Input(self.button_frame, self.vardict, self.bgdict)
        self.settings.grid(row=0, rowspan=4, column=4, sticky=N+W+E)
        self.settings.disable()
        if quitf: # there is a quit function, button right upper corner
            self.spacer_frame = Label(self.button_frame)
            self.spacer_frame.grid(row=0, column=9, sticky=N+W+E+S)
            self.spacer_frame.columnconfigure(9, weight=1)
            self.spacer_frame.rowconfigure(0, weight=1)
            self.button_frame.columnconfigure(9, weight=1)
            self.button_frame.rowconfigure(0, weight=1)
            self.quit_button = Button(self.button_frame, text="Quit", 
                                      pady=0,
                                      command=quitf)
            self.quit_button.grid(row=0, column=10, sticky=W+E)
        # Filter Frame
        self.filter_frame = Frame(self)
        self.filter_frame.grid(row=1, column=0, columnspan=4, sticky=W+N+S)
        label = Label(self.filter_frame, text="Filter: ")
        label.pack(side=LEFT)
        self.filter_selects = []
        for i in range(MAX_FILTERS):
            filter_select = FilterSelect(self.filter_frame, self.sensor, 
                                         self.filters, fid=i, 
                                         logger=self.logger)
            filter_select.pack(side=LEFT)
            self.filter_selects.append(filter_select)
        self.update_filter_button = Button(self.filter_frame,
                                           text="Reload",
                                           # pady=0,
                                           command=self.reload_filters)
        self.update_filter_button.pack(side=LEFT)
        # Visualizer
        self.visu_frame = Frame(self)
        self.visu_frame.grid(row=3, column=3, sticky=N+S)
        label = Label(self.visu_frame, text="Received")
        label.grid(row=0, column=0, sticky=N+W)
        self.visu_rec = VisualLogger(self.visu_frame)
        self.visu_rec.grid(row=1, column=0, sticky=N+S)        
        self.keyvisualizer_rec = KeyVisualizer(self.visu_frame)
        self.keyvisualizer_rec.grid(row=2, column=0)
        label = Label(self.visu_frame, text="Sent")
        label.grid(row=3, column=0, sticky=N+W)
        self.visu_sent = VisualLogger(self.visu_frame)
        self.visu_sent.grid(row=4, column=0, sticky=N+S)
        self.keyvisualizer_sent = KeyVisualizer(self.visu_frame)
        self.keyvisualizer_sent.grid(row=5, column=0)
        # stretching
        self.rowconfigure(3, weight=1) # the logger row
        self.columnconfigure(0, weight=1) # the logger column
        self.visu_frame.rowconfigure(1, weight=1) # received visualizer
        self.visu_frame.rowconfigure(4, weight=1) # sent visualizer

    def reload_filters(self):
        "callback to reload filters"
        self.logger.add("reloading filters")
        self.filters.reload_modules()

    def accept_data(self, state):
        "starts/stop receiving data from device"
        if state:
            port = int(self.vardict["IN_PORT"])
            if port < 32:
                s = "start accepting sensor data via bluetooth at"
            else:
                s = "start accepting sensor data via UDP at"
            self.logger.add(s, port)
            try:
                self.sensor.start_receiving(port)
            except Exception as data:
                self.logger.add("start failed", data)
        else:
            self.logger.add("stop accepting sensor data")
            self.sensor.stop_receiving()
        self.check_settings()
            
    def accept_log_data(self, state):
        "starts/stops logging received data"
        if state:
            self.logger.add("start logging received sensor data")
            self.sensor.register_rawsink(self.log_received)
        else:
            self.logger.add("stop logging received sensor data")
            self.sensor.unregister_rawsink(self.log_received)

    def log_received(self, packet):
        "callback to actually visualize received data"
        self.logger.add("received", packet)

    def accept_vis_data(self, state):
        "starts/stops visualizing received data"
        if state:
            self.logger.add("start visualizing accepted data")
            self.sensor.register_rawsink(self.visu_rec.update_dic)
            self.sensor.register_rawsink(self.keyvisualizer_rec.update_dic)
        else:
            self.logger.add("stop visualizing accepted data")
            self.sensor.unregister_rawsink(self.visu_rec.update_dic)
            self.sensor.unregister_rawsink(self.keyvisualizer_rec.update_dic)

    def send_data(self, state):
        "starts/stops sending filtered data"
        if state:
            host = self.vardict["OUT_HOST"]
            port = int(self.vardict["OUT_PORT"])
            self.logger.add("start sending data at host", host, "port", port)
            self.sensor.reg_send(out_host=host, out_port=port)
        else:
            self.logger.add("stop sending data")
            self.sensor.unreg_send()            
        self.check_settings()

    def send_log_data(self, state):
        "starts/stops logging sent data"
        if state:
            self.logger.add("start logging sent sensor data")
            self.sensor.register_sink(self.log_sent)
        else:
            self.logger.add("stop logging sent sensor data")
            self.sensor.unregister_sink(self.log_sent)

    def log_sent(self, packet):
        "callback displaying sent packet"
        self.logger.add("sent", packet)

    def send_vis_data(self, state):
        "starts/stops visualizing sent data"
        if state:
            self.logger.add("start visualizing sent data")
            self.sensor.register_sink(self.visu_sent.update_dic)
            self.sensor.register_sink(self.keyvisualizer_sent.update_dic)
        else:
            self.logger.add("stop visualizing sent data")
            self.sensor.unregister_sink(self.visu_sent.update_dic)
            self.sensor.unregister_sink(self.keyvisualizer_sent.update_dic)

    def record(self, state):
        "starts/stops recording to default file (date/time.sens)"
        if state:
            self.logger.add("start recording")
            self.sensor.reg_record()
        else:
            self.logger.add("stop recording")
            self.sensor.unreg_record()
        self.check_settings()
    
    def check_settings(self):
        "determine whether it is possible to change settings"
        if (self.accept_button.get() or 
            self.send_button.get() or 
            self.record_button.get()):
            self.settings.set_values(self.vardict)
            self.settings.disable()
        else:
            self.settings.enable()

    def end_it(self):
        "at the end of the app, stops the threads"
        self.sensor.stop_receiving()
        self.sensor.unregister_rawsink(self.log_received)
        self.sensor.unregister_rawsink(self.visu_rec.update_dic)
        self.sensor.unregister_rawsink(self.keyvisualizer_rec.update_dic)
        self.sensor.unregister_sink(self.visu_sent.update_dic)
        self.sensor.unregister_sink(self.keyvisualizer_sent.update_dic)
        self.sensor.unreg_record()


class ImageVisualizer(Frame):
    "Displays a snapshot image, no resizing"

    def __init__(self, master):
        Frame.__init__(self, master, width=160)
        self.image_height = 160
        self.image = PhotoImage("RGB", (160, 
                                        self.image_height))
        labelheight = self.image_height+6 # more space, allow for padding
        self.label = Label(self,
                           image=self.image,
                           pady=0,
                           width=160, 
                           height=labelheight)
        self.label.grid(row=0, column=0, sticky=N+S)
        self.columnconfigure(0, weight=1)

    def set_image(self, image):
        "displays the image"
        import Image as ImagePIL
        import StringIO
        img = ImagePIL.open(StringIO.StringIO(image))
        height = self.image_height
        width = 160
        rimg = img.resize((width, height))
        image = PhotoImage(rimg)
        self.label.configure(height=self.image_height+4)
        self.label["image"] = image
        self.image = image


class UIImgDist(Frame):
    "The main display of image distribution"

    def __init__(self, master=None):
        Frame.__init__(self, master, bd=2)
        PID = ImgDist
        self.imgdist_socket = PID.ImgDistSocket()
        self.imgdist_httpin = PID.ImgDistHTTP(port=PID.HTTP_INPORT)
        self.imgdist_httpout = PID.ImgDistHTTP(port=PID.HTTP_OUTPORT)
        self.vardict = {'SOCK_PORT': PID.SOCKET_PORT,
                        'HTTP_INPORT': PID.HTTP_INPORT,
                        'HTTP_OUTPORT': PID.HTTP_OUTPORT}
        self.lastimage_put = None
        self.create_widgets()
        self.init_snapshot()
        self.dist_http_button.invoke() # listen on http initially

    def create_widgets(self):
        "creates the entire image distribution panel"
        # Logger
        self.logger = Logger(self)
        self.logger.grid(row=3, column=0, columnspan=1, sticky=N+S+E+W)
        # Snapshot
        self.snapshot = ImageVisualizer(self)
        self.snapshot.grid(row=3, column=1, sticky=N+S)
        # Button Frame
        self.button_frame = Frame(self)
        self.button_frame.grid(row=0, column=0, columnspan=5, sticky=W)
        self.header = Header(self.button_frame, text="ImgDist")
        self.header.grid(row=0, rowspan=2, column=0, sticky=N+S)
        self.dist_socket_button = OnOffButton(self.button_frame, 
                                              "Acc/Dist Images Socket", 
                                              self.dist_socket)
        self.dist_socket_button.grid(row=0, column=1, sticky=W+E)
        self.dist_http_button = OnOffButton(self.button_frame, 
                                            "Acc/Dist Images HTTP", 
                                            self.dist_http)
        self.dist_http_button.grid(row=1, column=1, sticky=W+E)
        self.dist_log_acc_img_button = OnOffButton(self.button_frame, 
                                               "Log Acc Images", 
                                               self.dist_log_acc_img)
        self.dist_log_acc_img_button.grid(row=2, column=1, sticky=W+E)
        self.dist_log_dist_img_button = OnOffButton(self.button_frame, 
                                               "Log Dist Images", 
                                               self.dist_log_dist_img)
        self.dist_log_dist_img_button.grid(row=0, column=2, sticky=W+E)
        self.dist_log_all_button = OnOffButton(self.button_frame, 
                                               "Log all messages", 
                                               self.dist_log_all)
        self.dist_log_all_button.grid(row=1, column=2, sticky=W+E)
        self.snapshot_button = Button(self.button_frame, 
                                      text="Snapshot ImageStore", 
                                      command=self.take_snapshot,
                                      pady=0)
        self.snapshot_button.grid(row=2, column=2, sticky=W+E)
        # some settings
        self.settings = Input(self.button_frame, self.vardict)
        self.settings.grid(row=0, rowspan=4, column=4, sticky=N+W+E)
        self.settings.disable()
        # streching, only the column
        # self.rowconfigure(3, weight=1) # the logger row
        self.columnconfigure(0, weight=1) # the logger column

    def check_settings(self):
        "determine whether to allow chaning settings"
        if self.dist_socket_button.get() or self.dist_http_button.get():
            self.settings.set_values(self.vardict)
            self.settings.disable()
        else:
            self.settings.enable()

    def dist_socket(self, state):
        "starts/stops receiving images via sockets"
        if state:
            port = int(self.vardict["SOCK_PORT"])
            self.logger.add("start acc/dist images, socket, port", port)
            try:
                self.imgdist_socket.stop()
                self.imgdist_socket = ImgDist.ImgDistSocket(port=port)
                self.imgdist_socket.start()
            except Exception as data:
                self.logger.add("start acc/dist images socket failed", data)
        else:
            self.logger.add("stop acc/dist images via socket")
            try:
                self.imgdist_socket.stop()
            except Exception as data:
                self.logger.add("stop acc/dist images socket failed", data)
        self.check_settings()

    def dist_http(self, state):
        "starts/stops receiving images via http"
        if state:
            inport = int(self.vardict["HTTP_INPORT"])
            outport = int(self.vardict["HTTP_OUTPORT"])
            self.logger.add("start acc/dist images, http, ports ", 
                            inport, outport)
            try:
                self.imgdist_httpin.stop()
                self.imgdist_httpin = ImgDist.ImgDistHTTP(port=inport)
                self.imgdist_httpin.start()
                self.imgdist_httpout.stop()
                self.imgdist_httpout = ImgDist.ImgDistHTTP(port=outport)
                self.imgdist_httpout.start()
            except Exception as data:
                self.logger.add("start acc/dist images http failed", data)
        else:
            self.logger.add("stop acc/dist images http")
            try:
                self.imgdist_httpin.stop()
                self.imgdist_httpout.stop()
            except Exception as data:
                self.logger.add("stop acc/dist images http failed", data)
        self.check_settings()

    def dist_log_acc_img(self, state):
        "starts/stops logging of accepting images"
        if state:
            self.logger.add("start log image accepting")
            for imgdist in (self.imgdist_socket, 
                            self.imgdist_httpin, self.imgdist_httpout):
                imgdist.observers.add(ImgDist.ACCEPT, self.log_put)
        else:
            self.logger.add("stop log accepting images")
            for imgdist in (self.imgdist_socket, 
                            self.imgdist_httpin, self.imgdist_httpout):
                imgdist.observers.remove(ImgDist.ACCEPT, self.log_put)

    def dist_log_dist_img(self, state):
        "starts/stops logging of distributing images"
        if state:
            self.logger.add("start log image distributing")
            for imgdist in (self.imgdist_socket, 
                            self.imgdist_httpin, self.imgdist_httpout):
                imgdist.observers.add(ImgDist.DIST, self.log_get)
        else:
            self.logger.add("stop log distributing images")
            for imgdist in (self.imgdist_socket, 
                            self.imgdist_httpin, self.imgdist_httpout):
                imgdist.observers.remove(ImgDist.DIST, self.log_get)

    def dist_log_all(self, state):
        "starts/stops logging of messages created by ImgDist"
        if state:
            self.logger.add("start log all image dist messages")
            for imgdist in (self.imgdist_socket, 
                            self.imgdist_httpin, self.imgdist_httpout):
                imgdist.observers.add(ImgDist.LOG, self.log_dist)
        else:
            self.logger.add("stop log all image dist messages")
            for imgdist in (self.imgdist_socket, 
                            self.imgdist_httpin, self.imgdist_httpout):
                imgdist.observers.remove(ImgDist.LOG, self.log_dist)
        
    def log_dist(self, *pos):
        "the actually logging callback"
        self.logger.add(*pos)

    def log_get(self, img):
        "logging distributing one image"
        self.logger.add("distributed image of size", len(img))

    def log_put(self, img):
        "logging receiving one image"
        self.logger.add("accepted image of size", len(img))

    def init_snapshot(self):
        "called at the start to make snapshots possible"
        for imgdist in (self.imgdist_socket, 
                        self.imgdist_httpin, self.imgdist_httpout):
            imgdist.observers.add(ImgDist.ACCEPT, self.store_lastimage_put)

    def store_lastimage_put(self, img):
        "store latest put image"
        self.lastimage_put = img

    def take_snapshot(self):
        "displays latest put image"
        if self.lastimage_put:
            self.snapshot.set_image(self.lastimage_put)
        
    def end_it(self):
        "at the end of the program"
        for imgdist in (self.imgdist_httpout, self.imgdist_httpin,
                        self.imgdist_socket):
            imgdist.observers.remove(ImgDist.ACCEPT, 
                                     self.store_lastimage_put)
            imgdist.stop()


class Application(Frame):
    "The application"

    def __init__(self, master=None, filters=None):
        Frame.__init__(self, master)
        self.filters = filters
        self.grid(sticky=N+E+S+W) # display App Frame strechable
        self.create_widgets()

    def create_widgets(self):
        "create the panels, stretchable"
        self.ui_sensor = UISensor(self, 
                                  filters=self.filters, 
                                  quitf=self.end_it)
        self.ui_sensor.grid(column=0, row=0, sticky=N+S+E+W)
        self.ui_imgdist = UIImgDist(self)
        self.ui_imgdist.grid(column=0, row=1, sticky=N+S+E+W)
        # strechable
        self.columnconfigure(0, weight=1) # all
        self.rowconfigure(0, weight=1) # sensor
        # self.rowconfigure(1, weight=1) # imgdist
        # window closing event
        self._root().protocol("WM_DELETE_WINDOW", self.end_it)
        # make root window stretchable
        top = self.winfo_toplevel()
        top.rowconfigure(0, weight=1)
        top.columnconfigure(0, weight=1)

    def end_it(self):
        "at the end of the program call hoocks"
        self.ui_sensor.end_it()
        self.ui_imgdist.end_it()
        self.quit()

if __name__ == '__main__':
    if len(sys.argv) > 1:
        filters_instance=Filters(sys.argv[1])
    else:
        filters_instance = Filters()
    app = Application(filters=filters_instance)
    app.master.title("PySensor Control")
    app.mainloop()
